/*
 * Copyright (c) 2024 iSOFT INFRASTRUCTURE SOFTWARE CO., LTD.
 * easyAda is licensed under Mulan PubL v2.
 * You can use this software according to the terms and conditions of the Mulan PubL v2.
 * You may obtain a copy of Mulan PubL v2 at:
 *          http://license.coscl.org.cn/MulanPubL-2.0
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PubL v2 for more details.
 */


#include <arch/utils.h>
#include <stdlib/string.h>
#include <core/pbuddy.h>
#include <core/dprintf.h>

static void ppage_init(ppage_t *page, void *base, unsigned int size)
{
    int lb = biol1(size);
    assert(size == (size >> lb) << lb);
    page->flags  = 0;
    page->size   = size;
    page->lbsize = lb;
    page->base   = base;
}

static void psection_init(pbuddy_t *buddy, psection_t *sec, void *block, unsigned int pages, void **initptr,
                          int secindex, int expand)
{
    assert(!((long)block % (pages * CONFIG_PAGE_SIZE)));
    sec->begin = block;
    sec->size  = pages;
    sec->ltail = &sec->ltop;
    sec->pages = (*initptr);
    sec->ext   = 0;
    (*initptr) += (sizeof(ppage_t) * pages);
    memset(sec->pages, 0, sizeof(ppage_t) * pages);

    assert(pages <= CONFIG_PAGE_MAX);
    ppage_init(&sec->pages[0], block, pages);
    slist_append(buddy->ltail[biol1(pages)], sec->pages[0].b_next);
    slist_append(sec->ltail, sec->pages[0].s_next);
    sec->pages[0].secindex = secindex;
    return;
}

static void pbuddy_merge(pbuddy_t *buddy, ppage_t *page_l, ppage_t *page_r)
{
    assert(!page_l->flags);
    assert(page_l->secindex == page_r->secindex);

    if (page_r->flags) {
        return;
    }

    unsigned int pagesize = page_l->size * CONFIG_PAGE_SIZE;
    psection_t  *tob      = &buddy->secs[page_l->secindex];
    void        *sect_end = tob->begin + tob->size * CONFIG_PAGE_SIZE;
    ppage_t     *tomerge  = 0;
    ppage_t     *pn;
    BTOP_POP(buddy, page_l);

    page_l->lbsize++;
    page_l->size *= 2;
    pagesize *= 2;
    slist_push(buddy->ltop[page_l->lbsize], page_l->b_next);
    slist_pop(&page_l->s_next);

    if (page_l->lbsize == CONFIG_PAGE_MAX_INDEX) {
        return;
    }

    if (page_l->base + pagesize < sect_end) {
        tomerge = container_of(((page_r)->s_next), ppage_t, s_next);
    }

    int i;
    for (i = 0; i < tob->size; i++) {
        if (&tob->pages[i] == page_r) {
            tob->pages[i].base = 0;
            memset(&tob->pages[i], 0, sizeof(ppage_t));
            break;
        }
    }
    assert(i != tob->size - 1);

    if (!((long)page_l->base % (pagesize * 2))) {
        goto alignr;
    }

    tomerge = 0;
    slist_foreach(pn, tob->ltop, ppage_t, s_next)
    {
        if (page_l->base < pn->base + pagesize) {
            break;
        }
        if (page_l->base == pn->base + pagesize && (!pn->flags) && pn->lbsize == page_l->lbsize) {
            tomerge = pn;
            break;
        }
    }
    if (tomerge) {
        BTOP_POP(buddy, page_l);
        pbuddy_merge(buddy, tomerge, page_l);
    }
    return;
alignr:
    if (!tomerge || tomerge->flags) {
        return;
    }
    if (page_l->lbsize == tomerge->lbsize && page_l->base + pagesize == (tomerge)->base) {
        BTOP_POP(buddy, tomerge);
        pbuddy_merge(buddy, page_l, tomerge);
    }
    return;
}

void pbuddy_free_page(pbuddy_t *buddy, ppage_t *page)
{
    psection_t  *tobpush  = &buddy->secs[(page)->secindex];
    unsigned int pagesize = (page)->size * CONFIG_PAGE_SIZE;

    if (page->lbsize == CONFIG_PAGE_MAX_INDEX) {
        page->flags = 0;
        slist_push(buddy->ltop[(page)->lbsize], (page)->b_next);
        return;
    }

    if (!((long)page->base % (pagesize * 2))) {
        goto alignr;
    }

    ppage_t *tomer = 0, *pn;
    slist_foreach(pn, tobpush->ltop, ppage_t, s_next)
    {
        if ((page)->base < pn->base + pagesize) {
            break;
        }
        if ((page)->base == pn->base + pagesize && (!pn->flags) && pn->lbsize == page->lbsize) {
            assert(pn->lbsize == (page)->lbsize);
            tomer = pn;
            break;
        }
    }
    if (tomer) {
        page->flags = 0;
        pbuddy_merge(buddy, tomer, page);
        return;
    }
    page->flags = 0;
    slist_push(buddy->ltop[(page)->lbsize], (page)->b_next);
    return;

alignr:
    page->flags = 0;
    slist_push(buddy->ltop[page->lbsize], page->b_next);
    if ((page)->base + pagesize < tobpush->begin + tobpush->size * CONFIG_PAGE_SIZE) {
        tomer = container_of(((page)->s_next), ppage_t, s_next);
        if (page->lbsize != tomer->lbsize || (page)->base + pagesize != (tomer)->base || tomer->flags) {
            return;
        }

        BTOP_POP(buddy, tomer);
        pbuddy_merge(buddy, page, tomer);
    }
    return;
}

int pbuddy_find_page(pbuddy_t *buddy, void *ptr, ppage_t **page)
{
    if (!ptr || !page) {
        return -1;
    }
    int i;
    *page = 0;
    for (i = 0; i < buddy->seccount; i++) {
        if (buddy->secs[i].begin <= ptr && buddy->secs[i].begin + buddy->secs[i].size * CONFIG_PAGE_SIZE > ptr) {
            break;
        }
    }

    if (i == buddy->seccount) {
        return -2;
    }

    ppage_t *pn;
    slist_foreach(pn, buddy->secs[i].ltop, ppage_t,
                  s_next) if (pn->base <= ptr && pn->base + pn->size * CONFIG_PAGE_SIZE > ptr) *page = pn;
    if (!(*page)) {
        return -3;
    }
    if (!(*page)->flags) {
        *page = 0;
        return -4;
    }
    return 0;
}

int pbuddy_alloc_page(pbuddy_t *buddy, unsigned int pages, ppage_t **page)
{
    if (pages > CONFIG_PAGE_MAX) {
        *page = 0;
        return -1;
    }

    int lp = biol1(pages);

    assert(pages == ((pages >> lp) << lp));
    int rtn0;
    if (buddy->ltop[lp] != 0) {
        *page = container_of(slist_pop(&buddy->ltop[lp]), ppage_t, b_next);
        assert(!(*page)->flags);
        (*page)->flags = PG_LOCKED;
        return 0;
    } else {
        psection_t *tobpush;

        rtn0 = pbuddy_alloc_page(buddy, pages * 2, page);
        if (rtn0) {
            return rtn0;
        }
        ((*page)->lbsize)--;
        ((*page)->size) /= 2;

        tobpush = &buddy->secs[(*page)->secindex];
        int i;
        for (i = 0; i < (tobpush->size); i++) {
            if (!tobpush->pages[i].base) {
                break;
            }
        }
        assert(i < (tobpush->size));

        ppage_init(&tobpush->pages[i], (*page)->base + pages * CONFIG_PAGE_SIZE, pages);
        slist_push(buddy->ltop[(*page)->lbsize], tobpush->pages[i].b_next);
        slist_push((*page)->s_next, tobpush->pages[i].s_next);

        tobpush->pages[i].secindex = (*page)->secindex;
        return 0;
    }
}

pbuddy_t *pbuddy_init(void **block, unsigned int *pages, unsigned int seccount)
{
    int i, j;
    assert(!((long)block[0] % (pages[0] * CONFIG_PAGE_SIZE)));
    void     *initptr = block[0];
    pbuddy_t *buddy   = initptr;
    initptr += sizeof(pbuddy_t);

    buddy->seccount = seccount;
    buddy->secs     = initptr;
    initptr += (sizeof(psection_t) * seccount);
    memset(buddy->ltop, 0, (CONFIG_PAGE_MAX_INDEX + 1) * sizeof(void *));
    for (i = 0; i < CONFIG_PAGE_MAX_INDEX + 1; i++) {
        buddy->ltail[i] = &buddy->ltop[i];
    }

    for (i = 1; i < seccount; i++) {
        psection_init(buddy, &buddy->secs[i], block[i], pages[i], &initptr, i, 0);
    }

    void        *block0 = (void *)align_to((long)initptr, CONFIG_PAGE_SIZE) - CONFIG_PAGE_SIZE;
    unsigned int pages0 = pages[0] - ((long)(block0 - block[0]) / CONFIG_PAGE_SIZE);

    void *initptr0 = initptr;
    do {
        initptr = initptr0;
        block0 += CONFIG_PAGE_SIZE;
        pages0--;
        buddy->secs[0].begin = block0;
        buddy->secs[0].ext   = 0;
        buddy->secs[0].size  = pages0;
        buddy->secs[0].pages = initptr;
        buddy->secs[0].ltail = &buddy->secs[0].ltop;
        initptr += (sizeof(ppage_t) * pages0);
    } while (block0 < initptr);
    memset(buddy->secs[0].pages, 0, sizeof(ppage_t) * pages0);

    dprintf("total meta: %x\n", initptr - (void *)buddy);
    dprintf("sec[0] start: %x \n", block0);

    i = 0;
    j = 1;
    for (; pages0; pages0 >>= 1) {
        if (pages0 & 0x1) {
            ppage_init(&buddy->secs[0].pages[i], block0, j);
            slist_append(buddy->ltail[i], buddy->secs[0].pages[i].b_next);
            slist_append(buddy->secs[0].ltail, buddy->secs[0].pages[i].s_next);

            block0 += (j * CONFIG_PAGE_SIZE);
        }
        i++;
        j <<= 1;
    }
    return buddy;
}
